/** @file    PlayerPhonon.cpp
 *  @author  Alessandro Polo
 *  @version $Id: PlayerPhonon.cpp 2887 2010-08-11 08:09:41Z alex $
 *  @brief
 * File containing methods for the PlayerPhonon class.
 * The header for this class can be found in PlayerPhonon.h, check that file
 * for class description.
 ****************************************************************************/
/* Copyright (c) 2007-2010, WOSH - Wide Open Smart Home 
 * by Alessandro Polo - OpenSmartHome.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSmartHome.com WOSH nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY Alessandro Polo ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Alessandro Polo BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ****************************************************************************/
 
#include "PlayerPhonon.h"
#include "IPlayerPhononListener.h"

#include <core/Fact.h>
#include <core/FileSystem.h>

#include <fstream> // m3u parsing
#include <iostream> // m3u parsing
#include <algorithm> // folder parsing (sorting)

#include <QTime>

using namespace std;

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTORS

PlayerPhonon::PlayerPhonon( LogContext& log ) : QThread(NULL), Log(log)
 {
	this->playerListener = NULL;

	this->seekSlider = NULL;
	this->mediaObject = NULL;
	this->metaInformationResolver = NULL;
	this->audioOutput = NULL;
	this->volumeSlider = NULL;

	this->device_sink = "alsasink";
	this->device_output = "";

	this->currentRepeat = 0;
	this->playListRepeat = 0;

	this->volume_init = 40;
 }

PlayerPhonon::~PlayerPhonon()
 {
	Log(LOG_VERBOSE, ":~PlayerPhonon(): Destroying.." );

 }

//////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTORS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////// THREAD RUN

void PlayerPhonon::run()
 {
	Log(LOG_VERBOSE, ":runThread(): RUNNING" );

	this->audioOutput = new Phonon::AudioOutput(Phonon::MusicCategory, NULL);
	this->audioOutput->setVolume(this->volume_init/100.0);

	this->mediaObject = new Phonon::MediaObject(NULL);

	connect(this->mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)), this, SLOT(stateChanged(Phonon::State, Phonon::State)));
	connect(this->mediaObject, SIGNAL(currentSourceChanged(const Phonon::MediaSource&)), this, SLOT(sourceChanged(const Phonon::MediaSource&)));
	connect(this->mediaObject, SIGNAL(aboutToFinish()), this, SLOT(aboutToFinish()));
	connect(this->mediaObject, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged()));

	this->metaInformationResolver = new Phonon::MediaObject(NULL);
	connect(this->metaInformationResolver, SIGNAL(stateChanged(Phonon::State,Phonon::State)), this, SLOT(metaStateChanged(Phonon::State, Phonon::State)));

	Phonon::createPath(this->mediaObject, this->audioOutput);

	if ( this->playerListener != NULL )
		this->playerListener->changed_Volume( getVolume(), isMuted(), this );
	if ( this->playerListener != NULL )
		this->playerListener->changed_PlayState( 0, this );

	exec();

	delete this->metaInformationResolver; this->metaInformationResolver = NULL;
	delete this->mediaObject; this->mediaObject = NULL;
	delete this->audioOutput; this->audioOutput = NULL;

	Log(LOG_VERBOSE, ":exitingThread(): EXITING" );
 }

////////////////////////////////////////////////////////////////////////////////////////////////// THREAD RUN
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////// GSTREAMER EVENTS

void PlayerPhonon::stateChanged(Phonon::State newState, Phonon::State old_state )
 {
	(void)old_state;
	switch (newState) {
		case Phonon::ErrorState: {
				if (this->mediaObject->errorType() == Phonon::FatalError) {
					Log(LOG_WARNING, ":stateChanged() FatalError: %s", this->mediaObject->errorString().toLatin1().data() );
				 }
				break;
				}
		case Phonon::PlayingState: {
				// tell the listener we are playing
				if ( this->playerListener != NULL )
					this->playerListener->changed_PlayState( 1, this );
				break;
				}
		case Phonon::PausedState:
		case Phonon::StoppedState: {
				// tell the listener we are NOT playing
				if ( this->playerListener != NULL )
					this->playerListener->changed_PlayState( 0, this );
				}
		case Phonon::LoadingState: {
				Log(LOG_VERBOSE, ":stateChanged() : Loading ? percent done.." );
				break;
				}
		case Phonon::BufferingState: {
				Log(LOG_VERBOSE, ":stateChanged() : Buffering ? percent done.." );
				break;
				}
		default: break;
	 }
 }

void PlayerPhonon::sourceChanged(const Phonon::MediaSource& )
 {
	MediaTrack* track = this->playlist.getCurrentTrack();
	if ( track != NULL ) {
		track->setLength( this->mediaObject->totalTime() );
	 }
	if ( this->playerListener != NULL )	
		this->playerListener->changed_PlayItem( track, this );
 }

void PlayerPhonon::metaDataChanged()
 {
	MediaTrack* track = this->playlist.getCurrentTrack();
	if ( track == NULL )
		return;

	track->setLength( this->mediaObject->totalTime() );

	if ( !this->mediaObject->metaData("ARTIST").isEmpty() )
		track->setArtist( this->mediaObject->metaData("ARTIST").first().toLatin1().data() );

	if ( !this->mediaObject->metaData("ALBUM").isEmpty() )
		track->setAlbum( this->mediaObject->metaData("ALBUM").first().toLatin1().data() );

	if ( !this->mediaObject->metaData("TITLE").isEmpty() )
		track->setTitle( this->mediaObject->metaData("TITLE").first().toLatin1().data() );

	if ( !this->mediaObject->metaData("GENRE").isEmpty() )
		track->setGenre( this->mediaObject->metaData("GENRE").first().toLatin1().data() );

	if ( !this->mediaObject->metaData("TRACKNUMBER").isEmpty() )
		track->getProperties().set(_MediaTrack_KEY_TrackNumber, this->mediaObject->metaData("TRACKNUMBER").first().toLatin1().data() );

	if ( !this->mediaObject->metaData("DESCRIPTION").isEmpty() )
		track->getProperties().set(_MediaTrack_KEY_Description, this->mediaObject->metaData("DESCRIPTION").first().toLatin1().data() );
 }


void PlayerPhonon::metaStateChanged(Phonon::State newState, Phonon::State old_state )
{
	(void)old_state;

    if (newState == Phonon::ErrorState) {
        //QMessageBox::warning(this, tr("Error opening files"), metaInformationResolver->errorString());
        //while (!sources.isEmpty() && !(sources.takeLast() == metaInformationResolver->currentSource()));
        return;
    }

    if (newState != Phonon::StoppedState && newState != Phonon::PausedState)
        return;

    //if (metaInformationResolver->currentSource().type() == Phonon::MediaSource::Invalid)
    //     return;
}

void PlayerPhonon::aboutToFinish()
{
	if ( this->currentRepeat ) {
		Log(LOG_INFO, ":aboutToFinish() Repeating item.. [%d]", this->playlist.getCurrentIndex() );
		this->mediaObject->seek(0);
		return;
	 }
	if ( this->playlist.getCurrentIndex() < 0 || this->playlist.getTracks().size() == 0 ) {
		Log(LOG_INFO, ":aboutToFinish() : Finished." );
		return;
	 }
	// so: playListIndex is validated
	// did we reach end of playlist?
	if ( this->playlist.getCurrentIndex() == this->playlist.getTracks().size() -1 ) {
		Log(LOG_INFO, ":aboutToFinish() : End of Playlist" );
		// should we start again the playlist?
		if ( this->playListRepeat == 0 )
			return;
		play_index_(0, false );
		return;
	 }
	Log(LOG_INFO, ":aboutToFinish(%d) Playing next Item [of %d]", this->playlist.getCurrentIndex(), this->playlist.getTracks().size() );
	// ok..then play next item!
	play_index_( this->playlist.getCurrentIndex() + 1, true );
}

//////////////////////////////////////////////////////////////////////////////////////////// GSTREAMER EVENTS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////// INTERNAL ENQUEUE/PLAY

int PlayerPhonon::enqueue_( const std::string& file_url )
 {
	// may be recursive
	// protocol separator (file://path/to/resource; dvd://1; ..)
	// algorithm:
	//  1. fix the url
	//  2. parse it {may be invalid, folder, .m3u playlist}
	//  3. enqueue item(s)
	//  4. return playindex of first item or (-)ERROR#

	if ( file_url.size() < 3 ) {
		Log(LOG_CRITICAL, ":enqueue_(%s) : INVALID File!", file_url.c_str() );
		return -1;
	 }

	ResourcePath res_uri(file_url);
	bool isLocalResource = res_uri.isFile();
	std::string url_resolved = res_uri.resolve(true); // omit protocol
	Log(LOG_VERBOSE, ":enqueue_(%s) : Parsed[%d] to %s", file_url.c_str(), isLocalResource, url_resolved.c_str() );

	// get the associated index in the playlist
	int element_index = 0;
	if ( this->playlist.getTracks().size() > 0 )
		element_index = this->playlist.getTracks().size() - 1;

	// is it a file (audio or playlist) or a folder ?
	if ( isLocalResource )
	 {
		// is the resource.,. a FOLDER? we are kind.. and will add its files :)
		if ( FilePath::isFolder(url_resolved) ) {
			// inline creation of playlist, get files list
			std::vector<std::string> files;
			WRESULT ret = FileSystem::getFilesOf(url_resolved, files, false, false ); // NOT recursive, only filename
			if ( WFAILED(ret) || files.size() == 0 ) {
				Log(LOG_CRITICAL, ":enqueue_(%s): INVALID Folder [%d files]", url_resolved.c_str(), files.size() );
				return -2;
			 }
			// sort alphabetically
			std::sort( files.begin(), files.end() );
			Log(LOG_VERBOSE, ":enqueue_(%s): Analysing %d elements from folder", url_resolved.c_str(), files.size() );
			// enqueue each element [recursive call]
			std::vector<std::string>::const_iterator it;
			for ( it=files.begin(); it!=files.end(); it++ ) {
				enqueue_( FilePath::combine(res_uri.getPath(), *it) ); // recursive
			 }
			// anyway return the index of first added element
			return element_index;
		 }
		// is the resource a playlist file (M3U, Winamp)
		else if ( FilePath::isFile(url_resolved) && FilePath::getExtension(url_resolved, 1) == "M3U" ) {
			// open the playlist (text) file for read
			ifstream infile;
			infile.open( url_resolved.c_str(), ifstream::in );
			if ( !infile.is_open() ) {
				Log(LOG_CRITICAL, ":enqueue_(%s) : Invalid filename (can't open)", url_resolved.c_str() );
				return WRET_ERR_PARAM;
			 }
			// file is opened, let's parse contents and enqueue items
			Log(LOG_VERBOSE, ":enqueue_(%s): Loading Playlist..", url_resolved.c_str() );
 			// load the current system path, since media-paths are relative to current folder
			std::string base_dir = FilePath::getFolder( url_resolved );
			std::string buffer;
			size_t cPos;
			long found = 0;
			// walk the file
			while ( infile.good() ) {
				if ( infile.eof() ) break;
				getline( infile, buffer);
				// ignore comments, info and stuff
				cPos = buffer.find("#");
				if ( cPos != string::npos )
					continue;
				if ( buffer.size() < 3 )
					continue;
				Utilities::trimSpaces(buffer);
				Utilities::trimEndLine(buffer);
				buffer = FilePath::combine( base_dir, buffer );
				FilePath::toLocalPath(buffer);
				// we have a file url in the buffer, enqueue it (recursive call)
				int index = enqueue_( buffer );
				if ( index >= 0 )
					++found;
			 }
			Log(LOG_INFO, ":enqueue_(%s) : Loaded %d files from %s", res_uri.getPath().c_str(), found, url_resolved.c_str() );
			// we are done :)
			infile.close();
			// return the index of first added element
			return element_index;
		 }
		else if ( !FilePath::isFile(url_resolved) ) {
			// an invalid filesystem URI!?
			Log(LOG_CRITICAL, ":enqueue_(%s) : Local file '%s' was not found!", res_uri.getPath().c_str(), url_resolved.c_str() );
			return -3;
		 }

	 }
	// then it's a valid file/url, this is often called (recursive) by previous blocks

	// create a new item
	MediaTrack* item = new MediaTrack(res_uri);
	this->playlist.getTracks().push_back(item);

	Log(LOG_INFO, ":enqueue_(%s) : Enqueued #%d [%d]", res_uri.getPath().c_str(), element_index, this->playlist.getTracks().size()-1 );
	// return the index of last element in the playlist (just added it)
	return element_index;
 }

WRESULT PlayerPhonon::play_index_( int index, bool synch )
 {
	// validate input
	if ( this->playlist.getTracks().size() == 0 ) {
		Log(LOG_CRITICAL, ":play_index_(%d) FAILED : Empty Playlist!", index );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( index < 0 ) {
		Log(LOG_CRITICAL, ":play_index_() FAILED : %d Out of Range!", index );
		return WRET_ERR_PARAM;
	 }
	if ( index >= (int)this->playlist.getTracks().size() ) {
		Log(LOG_CRITICAL, ":play_index_() FAILED : %d Out of (Playlist) Range!", index );
		return WRET_ERR_PARAM;
	 }
	// update current index and get the associated Item
	this->playlist.setCurrentIndex(index);
	MediaTrack* item = this->playlist.getCurrentTrack();
	if ( item == NULL ) {
		Log(LOG_CRITICAL, ":play_index_() FAILED : Item#%d not found!", index );
		return WRET_ERR_INTERNAL;
	 }
	// finally play the item
	return play_( item, synch );
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::play_( MediaTrack* item, bool synch )
 {
	(void)synch;
	if ( item == NULL ) return WRET_ERR_PARAM;
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":play(): Thread not running" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	// called while still playing, let's stop the media, but can't wait when if called in gst bus thread?
	if ( isPlaying() ) {
		Log(LOG_INFO, ":play_(%s): was playing, pausing..", item->getTrack().getPathFull().c_str() );
	 }
	this->mediaObject->stop();
	this->mediaObject->clearQueue();

	string url_resolved;
	if ( item->getTrack().isFile() ) {
		url_resolved = item->getTrack().resolve(true); // omit protocol (file://, ..)
		if ( !FilePath::exists(url_resolved) ) {
			Log(LOG_CRITICAL, ":play(): File %s not found, resolved %s", item->getTrack().getPath().c_str(), url_resolved.c_str() );
			return WRET_ERR_PARAM;
		 }
	 }
	else
		url_resolved = item->getTrack().resolve(false); // keep protocol (http://, ..)

	if ( url_resolved.empty() ) {
		///@bug BUG HERE
		Log(LOG_INFO, ":play_(%s):BUG HERE..", item->getTrack().getPath().c_str() );
		return WRET_ERR_PARAM;
	 }

	Log(LOG_INFO, ":play_(%s): Playing resolved %s", item->getTrack().getPath().c_str(), url_resolved.c_str() );

	Phonon::MediaSource mSource( QString(url_resolved.c_str()) );
	this->mediaObject->setCurrentSource(mSource);

	this->mediaObject->play();
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::play( const std::string& file_url )
 {
	if ( file_url.empty() ) {
		// GST asynch
		if ( !isRunning() ) {
			Log(LOG_CRITICAL, ":play(): Thread not running" );
			return WRET_ERR_ILLEGAL_USE;
		 }
		if ( isPlaying() ) {
			Log(LOG_CRITICAL, ":play(""): already playing!" );
			return WRET_ERR_ILLEGAL_USE;
		 }
		Log(LOG_VERBOSE, ":play(): setting PLAYING state.." );
		// set GST state, listeners may be informed asynch (see gst_StateChanged)
		this->mediaObject->play();
		return WRET_OK;
	 }
	// we enqueue file and play returned index (a negative value means an error)
	MutexLockerWrite mL(this->playlist.getTracks().getMutex());
	int index = enqueue_(file_url);
	if ( index < 0 ) {
		Log(LOG_CRITICAL, ":play(%s): Invalid Item", file_url.c_str() );
		return WRET_ERR_PARAM;
	 }
	return play_index_(index, false);
 }

/////////////////////////////////////////////////////////////////////////////////////// INTERNAL ENQUEUE/PLAY
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////// PLAYER METHODS

WRESULT PlayerPhonon::enqueue( const std::string& file_url )
 {
	 MutexLockerWrite mL(this->playlist.getTracks().getMutex());
	int index = enqueue_(file_url);
	if ( index < 0 )
		return WRET_ERR_INTERNAL;

	if ( this->playerListener != NULL )
		this->playerListener->changed_PlayList( &this->playlist, this );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::clear()
 {
	// clear playlist, releasing objects
	this->playlist.clear();
	Log(LOG_INFO, ":clear() Playlist cleaned." );

	if ( this->playerListener != NULL )
		this->playerListener->changed_PlayList( &this->playlist, this );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::first() {
	return play_index_( 0, false );
 }

WRESULT PlayerPhonon::last() {
	return play_index_( this->playlist.size() - 1, false );
 }

WRESULT PlayerPhonon::prev() {
	if ( this->playlist.getCurrentIndex() < 1 )
		return WRET_ERR_ILLEGAL_USE;
	return play_index_( this->playlist.getCurrentIndex() - 1, false );
 }

WRESULT PlayerPhonon::next() {
	return play_index_( this->playlist.getCurrentIndex() + 1, false );
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::remove( int index )
 {
	MutexLockerWrite mL(this->playlist.getTracks().getMutex());
	this->playlist.remove(index);

	if ( this->playerListener != NULL )
		this->playerListener->changed_PlayList( &this->playlist, this );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::play( int index )
 {
	return play_index_( index, false );
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::pause()
 {
	// GST asynch (NULL GST_STATE_PAUSED)
	// validate gst status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":pause(): Thread not running" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( this->mediaObject == NULL ) {
		Log(LOG_CRITICAL, ":pause(): NULL mediaObject" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	this->mediaObject->pause();
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

WRESULT PlayerPhonon::stop()
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":stop(): Thread not running" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( this->mediaObject == NULL ) {
		Log(LOG_CRITICAL, ":stop(): NULL mediaObject" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( isPlaying() ) {
		this->mediaObject->stop();
	 }
	else Log(LOG_WARNING, ":stop(): NOT playing" );

	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

bool PlayerPhonon::isPlaying()
 {
	// validate status
	if ( !isRunning() ) return false;
	if ( this->mediaObject == NULL ) {
		Log(LOG_CRITICAL, ":isPlaying(): NULL mediaObject" );
		return false;
	 }
	return ( this->mediaObject->state() == Phonon::PlayingState );
 }

////////////////////////////////////////////////////////////////////////////////////////////// PLAYER METHODS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////// VOLUME CONTROL

WRESULT PlayerPhonon::setVolume( int newVolume )
 {
	// validate our status
	if ( !isRunning() ) {
		// we will have to wait till thread is running to apply the new setting!
		// save the output (name) to local var
		this->volume_init = newVolume;
		Log(LOG_VERBOSE, ":setVolume(%d) [really applied when started]", this->volume_init );
		return WRET_OK;
	 }
	if ( this->audioOutput == NULL ) {
		Log(LOG_CRITICAL, ":setVolume(): NULL audioOutput" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( newVolume < 0 ) {
		Log(LOG_CRITICAL, ":setVolume(): Invalid negative value %d (/100)", newVolume );
		return WRET_ERR_PARAM;
	 }
	Log(LOG_INFO, ":setVolume(%d)", newVolume );

	// convert the volume level [0,100] to range [0,1] and apply the new value
	qreal vol = newVolume / 100.0;
	this->audioOutput->setVolume(vol);

	if ( this->playerListener != NULL )
		this->playerListener->changed_Volume( vol*100, this->audioOutput->isMuted(), this );
	return WRET_OK;
 }

WRESULT PlayerPhonon::setVolumeOffset( int volume_span )
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":setVolumeOffset(): Thread not running" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( this->audioOutput == NULL ) {
		Log(LOG_CRITICAL, ":setVolumeOffset(): NULL audioOutput" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	// get the current value
	qreal vol = this->audioOutput->volume();
	if ( vol < 0 ) {
		Log(LOG_CRITICAL, ":setVolumeOffset(%d): FAILED getting current value", volume_span );
		return WRET_ERR_INTERNAL;
	 }
	// add (subtract) the (converted) span level to current level, fix the range
	vol += volume_span / 100.0;
	if ( vol < 0.0 )			vol = 0;
	else if ( vol > 100.0 )	vol = 100.0;
	Log(LOG_VERBOSE, ":setVolumeOffset(%d): %d (/100)", volume_span, vol*100 );
	// apply the new volume
	this->audioOutput->setVolume(vol);

	if ( this->playerListener != NULL )
		this->playerListener->changed_Volume( vol*100, this->audioOutput->isMuted(), this );
	return WRET_OK;
 }

WRESULT PlayerPhonon::setMute( bool on )
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":setMute(): Thread not running" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	if ( this->audioOutput == NULL ) {
		Log(LOG_CRITICAL, ":setMute(): NULL audioOutput" );
		return WRET_ERR_ILLEGAL_USE;
	 }
	Log(LOG_INFO, ":setMute(%d)", on );
	this->audioOutput->setMuted(on);

	if ( this->playerListener != NULL )
		this->playerListener->changed_Volume( this->audioOutput->volume()*100, this->audioOutput->isMuted(), this );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

int PlayerPhonon::getVolume()
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":getVolume(): Thread not running" );
		return -1;
	 }
	if ( this->audioOutput == NULL ) {
		Log(LOG_CRITICAL, ":getVolume(): NULL audioOutput" );
		return -1;
	 }
	return (this->audioOutput->volume() * 100);
 }

bool PlayerPhonon::isMuted()
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":isMuted(): Thread not running" );
		return false;
	 }
	if ( this->audioOutput == NULL ) {
		Log(LOG_CRITICAL, ":isMuted(): NULL audioOutput" );
		return false;
	 }
	return this->audioOutput->isMuted();
 }

////////////////////////////////////////////////////////////////////////////////////////////// VOLUME CONTROL
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////// POSITION CONTROL

WRESULT PlayerPhonon::setPosition( float position_rate )
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":setPosition(): Thread not running" );
		return -1;
	 }
	if ( this->mediaObject == NULL ) {
		Log(LOG_CRITICAL, ":setPosition(): NULL mediaObject" );
		return -1;
	 }
	if  ( position_rate < 0.0 || position_rate > 1.0 ) {
		Log(LOG_CRITICAL, ":setPosition(%f): Out of range [0,1]", position_rate );
		return -2;
	 }

	// get the length of current media
	qint64 pos = this->mediaObject->currentTime();
	// calculate the new position and try to apply
	pos = (float) pos * position_rate;
	this->mediaObject->seek(pos);
	Log(LOG_INFO, ":setPosition(%f): Seek %"PRId64" ms", position_rate, pos );
	return WRET_OK;
 }

WRESULT PlayerPhonon::setPositionOffset( float position_rate_offset )
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":setPositionOffset(): Thread not running" );
		return -1;
	 }
	if ( this->mediaObject == NULL ) {
		Log(LOG_CRITICAL, ":setPositionOffset(): NULL mediaObject" );
		return -1;
	 }
	if  ( position_rate_offset < -1.0 || position_rate_offset > 1.0 ) {
		Log(LOG_CRITICAL, ":setPositionOffset(%f): Out of range [-1,1]", position_rate_offset );
		return -2;
	 }

	// get the length of current media
	qint64 pos = this->mediaObject->currentTime();
	// calculate the new position and try to apply
	pos = pos + (float)pos * position_rate_offset;
	this->mediaObject->seek(pos);
	Log(LOG_INFO, ":setPositionOffset(%f): Seek %"PRId64" ms", position_rate_offset, pos );
	return WRET_OK;
 }

WRESULT PlayerPhonon::setPositionMs( unsigned long position_ms )
 {
	// validate status
	if ( !isRunning() ) {
		Log(LOG_CRITICAL, ":setPositionMs(): Thread not running" );
		return -1;
	 }
	if ( this->mediaObject == NULL ) {
		Log(LOG_CRITICAL, ":setPositionMs(): NULL mediaObject" );
		return -1;
	 }
	if  ( position_ms < 0.0 ) {
		Log(LOG_CRITICAL, ":setPositionMs(%ld): Invalid negative value", position_ms );
		return -2;
	 }
	// get the length of current media
	qint64 pos = this->mediaObject->currentTime();
	// calculate the new position and try to apply
	pos = pos + position_ms;
	this->mediaObject->seek(pos);
	Log(LOG_INFO, ":setPositionMs(%ld): Seek %"PRId64" ms", position_ms, pos );
	return WRET_OK;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////

float PlayerPhonon::getPosition()
 {
	if ( this->mediaObject == NULL )
		return 0.0;
	// get the length of current media and its current position
	// calculate and return the relative position
	qint64 tot_msec = this->mediaObject->totalTime();
	return (float)tot_msec / (float)this->mediaObject->currentTime();
 }

unsigned long PlayerPhonon::getPositionMs()
 {
	if ( this->mediaObject == NULL )
		return 0;
	// get current position (milliseconds)
	return (unsigned long)this->mediaObject->currentTime();
 }

string PlayerPhonon::getPositionStr()
 {
	if ( this->mediaObject == NULL )
		return "";
	qint64 tot_msec = this->mediaObject->currentTime();
	QTime time(0,0,0,0);
	time = time.addMSecs( tot_msec );
	return time.toString("hh:mm:ss").toLatin1().data();
 }

//////////////////////////////////////////////////////////////////////////////////////////// POSITION CONTROL
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

void PlayerPhonon::setRepeatItem( bool value )
 {
	this->currentRepeat = (short)value;
 }

void PlayerPhonon::setRepeatPlayList( bool value )
 {
	this->playListRepeat = (short)value;
 }

WRESULT PlayerPhonon::setDeviceOutput( const string& )
 {
	return WRET_ERR_ILLEGAL_USE;
 }

WRESULT PlayerPhonon::setDeviceDriver( const string& )
 {
	return WRET_ERR_ILLEGAL_USE;
 }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////// STATIC METHODS

string PlayerPhonon::getPhonon2String( Phonon::State state )
 {
	switch(state) {
		case Phonon::ErrorState:		return "ErrorState";
		case Phonon::PlayingState:		return "PlayingState";
		case Phonon::PausedState:		return "PausedState";
		case Phonon::StoppedState:		return "StoppedState";
		case Phonon::BufferingState:	return "BufferingState";
		default:						return "UNKNOWN";
	 }
 }

////////////////////////////////////////////////////////////////////////////////////////////// STATIC METHODS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
